This vignette will introduce you to Monocle 3, an R package that provides a toolkit for the analysis of single-cell expression and accessibility data. We will use it for constructing single-cell accessibility trajectories and pseudotime analyses, and combine it with the previously calculated TF activity scores to identify TFs that may change their activity in a trajectory-dependent way! An exciting set of analyses and integrations is ahead of us.
This vignette is a modified version of this vignette and this vignette as well as this website.
As before, we first load all necessary R packages that are needed for this vignette.
suppressPackageStartupMessages({
library(Signac)
library(Seurat)
library(SeuratWrappers)
library(monocle3)
library(tidyverse)
library(patchwork)
library(scales)
library(ComplexHeatmap)
library(circlize)
library(RColorBrewer)
})
set.seed(1990)
We begin by loading our pre-processed Seurat object. In
most vignettes, we took the object from the introductory QC vignette
into R; however here, we will use the final object from the TF analysis
vignette because we will need the chromAVR derived TF
activity scores here as well!
# Specify the path to the "outs" folder from cellranger-atac here
# Make sure to have a trailing slash here
outFolder="/mnt/data/cellranger/outs/"
# This time, we load a different object at the beginning
seu.s = readRDS(file = paste0(outFolder,"obj.filt.TFAnalysis.rds"))
DefaultAssay(seu.s) <- 'ATAC'
# Let's make sure we are working with correct clusters
Idents(seu.s) = seu.s$ATAC_snn_res.0.5
MonocleBefore we start, let us recap some general information about the
Monocle 3 package. It has been designed for single-cell
RNA-seq data, and its main types of analysis are: - Clustering,
classifying, and counting cells. scRNA-Seq experiments allow you to
discover new (and possibly rare) subtypes of cells. - Constructing
single-cell trajectories. In development, disease, and throughout life,
cells transition from one state to another. - Differential expression
analysis. Characterizing new cell types and states begins with
comparisons to other
In this vignette, we will mainly focus on constructing single-cel
accessibility trajectories, which in combination with the
Cicero package can also be used with scATAC-seq data. The
first type of analysis we have already done before for scATAC-seq data,
while the last analysis - differential expression analysis - can be
replaced by differential accessibility analyses also with the help of
Cicero.
cell_data_set formattraj.cds <- as.cell_data_set(seu.s, graph = "ATAC_snn")
## Warning: Monocle 3 trajectories require cluster partitions, which Seurat does
## not calculate. Please run 'cluster_cells' on your cell_data_set object
traj.cds <- cluster_cells(cds = traj.cds, reduction_method = "UMAP", graph = "ATAC_snn")
Monocle3 aims to learn how cells transition through a
biological program of gene expression changes in an experiment. Each
cell can be viewed as a point in a high-dimensional space, where each
dimension describes the expression of a different gene. Identifying the
program of gene expression changes is equivalent to learning a
trajectory that the cells follow through this space. However, the more
dimensions there are in the analysis, the harder the trajectory is to
learn. Fortunately, many genes typically co-vary with one another, and
so the dimensionality of the data can be reduced with a wide variety of
different algorithms. Monocle3 provides two different
algorithms for dimensionality reduction via Monocle3 (UMAP
and tSNE). Both take a Monocle3 object and a number of
dimensions allowed for the reduced space. You can also provide a model
formula indicating some variables (e.g. batch ID or other technical
factors) to “subtract” from the data so it doesn’t contribute to the
trajectory. The function Monocle3 is the fourth step in the
trajectory building process after preprocess_cds,
reduce_dimension, and cluster_cells. After
learn_graph, order_cells is typically
called.
minimal_branch_len 1To get a better understanding and feeling for the data, we are now
exploring the effect of one of the parameters for the
learn_graph function: The customization that is possible
via the learn_graph_control parameter, which contains a
subparameter called minimal_branch_len. First, use the R
help to understand what it does!
As you have read by now (hopefully), minimal_branch_len
denotes the length of the diameter path for a branch to be preserved
during graph pruning procedure, with a default is 10. Here, we try a
minimal value of 1 first:
After learning the graph, let’s first plot the cells along with their
trajectories to get a general overview. At this point, we first have to
see the general graph before deciding on a potential root node - the
origin of the trajectory. Without this guess, we cannot call the
order_cells function - the last step in the aforementioned
Monocle3 trajectory building process!
traj.cds <- learn_graph(traj.cds, use_partition = TRUE, close_loop = FALSE,
learn_graph_control = list(rann.k = 20, minimal_branch_len = 1))
## Warning in igraph::graph.dfs(stree_ori, root = root_cell, neimode = "all", :
## Argument `neimode' is deprecated; use `mode' instead
# Plots the cells along with their trajectories.
plot_cells(cds = traj.cds, show_trajectory_graph = TRUE,
color_cells_by = "ATAC_snn_res.0.5",
label_cell_groups = TRUE,
cell_size = 0.75, alpha = .4,
label_principal_points = FALSE)
## Warning: `select_()` was deprecated in dplyr 0.7.0.
## Please use `select()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was generated.
minimal_branch_len 3Let’s repeat the same, but with a higher value for
minimal_branch_len. If you want, create your own function
that runs both learn_graph and plot_cells with
minimal_branch_len as a paramter to avoid code
redundancy!
traj.cds <- learn_graph(traj.cds, use_partition = TRUE, close_loop = FALSE,
learn_graph_control = list(rann.k = 20, minimal_branch_len = 3))
## Warning in igraph::graph.dfs(stree_ori, root = root_cell, neimode = "all", :
## Argument `neimode' is deprecated; use `mode' instead
plot_cells(cds = traj.cds, show_trajectory_graph = TRUE,
color_cells_by = "ATAC_snn_res.0.5",
label_cell_groups = TRUE,
cell_size = 0.75, alpha = .4,
label_principal_points = FALSE)
# Learn what the two parameters that we change here do!
plot_cells(cds = traj.cds, show_trajectory_graph = TRUE,
color_cells_by = "ATAC_snn_res.0.5",
label_cell_groups = FALSE,
cell_size = 0.75, alpha = .4,
label_principal_points = TRUE)
The black lines show the structure of the graph. Note that the graph is not fully connected: cells in different partitions are in distinct components of the graph. The circles with numbers in them denote special points within the graph. Each leaf, denoted by light gray circles, corresponds to a different outcome (i.e. cell fate) of the trajectory. Black circles indicate branch nodes, in which cells can travel to one of several outcomes. You can control whether or not these are shown in the plot with the label_leaves and label_branch_points arguments to plot_cells. Please note that numbers within the circles are provided for reference purposes only.
Now that we have a sense of where the early cells fall, we can call
order_cells(), which will calculate where each cell falls
in pseudotime. In order to do so order_cells() needs you to
specify the root nodes of the trajectory graph. If you don’t provide
them as an argument, it will launch a graphical user interface for
selecting one or more root nodes (see the line that is commented out in
the chunk above).
Note that some of the cells are gray. This means they have infinite pseudotime, because they were not reachable from the root nodes that were picked. In general, any cell on a partition that lacks a root node will be assigned an infinite pseudotime. In general, you should choose at least one root per partition.
Once we’ve learned a graph, we are ready to order the cells according
to their progress through the developmental program.
Monocle measures this progress in pseudotime, a
measure of how much progress an individual cell has made through a
process such as cell differentiation.
Let’s dive a bit more into the concept of pseudotime. In many biological processes, cells do not progress in perfect synchrony. In single-cell expression studies of processes such as cell differentiation, captured cells might be widely distributed in terms of progress. That is, in a population of cells captured at exactly the same time, some cells might be far along, while others might not yet even have begun the process. This asynchrony creates major problems when you want to understand the sequence of regulatory changes that occur as cells transition from one state to the next. Tracking the expression across cells captured at the same time produces a very compressed sense of a gene’s kinetics, and the apparent variability of that gene’s expression will be very high.
By ordering each cell according to its progress along a learned
trajectory, Monocle alleviates the problems that arise due
to asynchrony. Instead of tracking changes in expression as a function
of time, Monocle tracks changes as a function of progress
along the trajectory, which is termed pseudotime. Pseudotime is
an abstract unit of progress: it’s simply the distance between a cell
and the start of the trajectory, measured along the shortest path. The
trajectory’s total length is defined in terms of the total amount of
transcriptional change that a cell undergoes as it moves from the
starting state to the end state.
Thus, we can assigns cells a pseudotime value based on their
projection on the principal graph learned in the
learn_graph function and the position of chosen root
states. This function takes as input a cell_data_set and
returns it with pseudotime information stored internally.
order_cells() optionally takes “root” state(s) in the form
of cell or principal graph node IDs, which you can use to specify the
start of the trajectory. If you don’t provide a root state, an plot will
be generated where you can choose the root state(s) interactively. The
trajectory will be composed of segments.
How do we identify the starting point of differentiation, which is
often denoted the root node for trajectory analyses? In
order to place the cells in order, we need to tellMonocle
where the “beginning” of the biological process is. We do so by choosing
regions of the graph that we mark as “roots” of the trajectory. In time
series experiments, this can usually be accomplished by finding spots in
the UMAP space that are occupied by cells from early time points. Here,
for our data and the biology, one strategy is to look for pluripotency
signatures, which indicates here that cluster 1 may be the
origin.
# If you dont provide root_pr_nodes, an interactive window opens
# traj.cds <- order_cells(traj.cds, reduction_method = "UMAP")
traj.cds <- order_cells(traj.cds, reduction_method = "UMAP", root_pr_nodes = c("Y_15","Y_28","Y_3"))
# plot trajectories colored by pseudotime
plot_cells(cds = traj.cds, show_trajectory_graph = TRUE,
color_cells_by = "pseudotime",
cell_size = 0.75, alpha = .4,
label_principal_points = TRUE)
## Cells aren't colored in a way that allows them to be grouped.
We just ordered cells and identified different potential
trajectories! Let us explore them in more detail! For this, we will use
choose_graph_segments to choose cells along the path of a
principal graph. This allows us to essentially group the cells into
different subsets, based on specific trajectories.
In general, it’s often desirable to specify the root of the
trajectory programmatically, rather than manually picking it as we did
above. This can be done by first grouping the cells according to which
trajectory graph node they are nearest to. Then, one can calculate what
fraction of the cells at each node come from the earliest time point.
Then one picks the node that is most heavily occupied by early cells and
returns that as the root. For a specific example, take a look at the
function get_earliest_principal_node here.
A task for those who like being challenged: Can you adapt the
function for your data and identify which root node it
picks?
In general, it is often useful to subset cells based on their branch in the trajectory. let’s try this out now in practise!
From the data we gathered, one could argue that cluster 3 has cells differentiating into the mesoderm lineage (vascular, immune related genes/TFs), and a lot of activity-induced Jun/Fos genes. Let’s create a hypothetical mesoderm-specific cell subset. Because we will re-use the same code for other cell subsets, we again write a small custom function here which takes the set of starting and ending nodes as input and outputs a subset object as well as some plots.
subsetAndPlotTrajectory <- function(obj, startingNodes, endNodes) {
# traj.cds.sub = choose_graph_segments(traj.cds, clear_cds = F) # interactive
obj.sub = choose_graph_segments(obj, clear_cds = FALSE,
starting_pr_node = startingNodes, # only one
ending_pr_nodes = endNodes)
# We can now order the cells based on the subset
obj.sub <- order_cells(obj.sub, reduction_method = "UMAP", root_pr_nodes = startingNodes)
plot_cells(cds = obj.sub, show_trajectory_graph = TRUE,
color_cells_by = "pseudotime",
cell_size = 0.75, alpha = .4,
label_principal_points = FALSE) %>% plot()
plot_cells(cds = obj.sub, show_trajectory_graph = TRUE,
color_cells_by = "ATAC_snn_res.0.5",
label_cell_groups = FALSE,
cell_size = 0.75, alpha = .4,
label_principal_points = FALSE) %>% plot()
obj.sub
}
We can now try and run our new function:
meso.traj.sub = subsetAndPlotTrajectory(traj.cds, startingNodes = c("Y_3"), endNodes = c("Y_23"))
## Cells aren't colored in a way that allows them to be grouped.
Let’s do the same for potentially neuronal cells:
neu.traj.sub = subsetAndPlotTrajectory(traj.cds, startingNodes = c("Y_3"), endNodes = c("Y_16","Y_13","Y_32"))
## Cells aren't colored in a way that allows them to be grouped.
Lastly, a group of heterogeneous cells:
und.traj.sub = subsetAndPlotTrajectory(traj.cds, startingNodes = c("Y_3"), endNodes = c("Y_4"))
## Cells aren't colored in a way that allows them to be grouped.
We will now try to combine the pseudotime and trajectory analyses in
combination with the chromVAR derived TF activity scores to
find out whether some TFs are changing their activity with
pseudotime.
Seurat objectLet’s first add the pseudotime to our original Seurat
object in the form of additional metadata! This will allow us to color
cells by their pseudotime, subset the object according to it, and so
forth. We will create 3 groups here, one for each of our hypothesized
cell populations!
# Add pseudotime information from each of the 3 sub-populations we identified before
seu.s <- AddMetaData(
object = seu.s,
metadata = neu.traj.sub@principal_graph_aux@listData$UMAP$pseudotime,
col.name = "pseudotime.neu"
)
seu.s <- AddMetaData(
object = seu.s,
metadata = meso.traj.sub@principal_graph_aux@listData$UMAP$pseudotime,
col.name = "pseudotime.meso"
)
seu.s <- AddMetaData(
object = seu.s,
metadata = und.traj.sub@principal_graph_aux@listData$UMAP$pseudotime,
col.name = "pseudotime.und"
)
# Create a separate Seurat object for each trajectory
seu.sub.neu = subset(seu.s, subset = pseudotime.neu > 0)
seu.sub.meso = subset(seu.s, subset = pseudotime.meso > 0)
seu.sub.und = subset(seu.s, subset = pseudotime.und > 0)
Now, we will try to visualize the TF activity that we derived before
in the TF analysis vignette and use the chromVAR scores for
the accessibility trajectories. In order to minimize code repetition, we
will again write a custom function so that we can call it for different
TFs and the different cell subsets we created before in an easy and
reproducible manner:
visualizeTFTrajectory <- function (TF, obj, pseudotimeColName, trajectoryName, ylim = c(-5,7)) {
df = tibble(pseudotime = obj@meta.data[[c(pseudotimeColName)]],
TF = obj@assays$chromvar@data[TF,]) %>%
mutate(pseudotime.sc = rescale(pseudotime, to = c(0,100)))
# ylim = c(floor(range(df$TF)[1]),ceiling(range(df$TF)[2]))
g = ggplot(df, aes(x = pseudotime.sc, y = TF, color = pseudotime.sc)) +
geom_point() +
xlab("Pseudotime, %") + ylab("chromVAR: TF activity") + ylim(ylim) +
scale_color_viridis_c(option = "B") +
geom_smooth(method = "loess") + # Jitter on x-axis a bit
ggtitle(trajectoryName)
plot(g)
}
Feel free to customize this function even further and to add function
parameters for increased flexibility. For example, you may want to add
an argument that determines the rescaling in a more flexible manner
rather than the hard-coded c(0,100) in the current version,
or the pseudotime coloring.
Nevertheless, let’s try our new function for some TFs. We start with
PITX1-MOUSE.H11MO.0.C:
TF = "PITX1-MOUSE.H11MO.0.C"
visualizeTFTrajectory(TF = TF,
obj = seu.sub.neu, pseudotimeColName = "pseudotime.neu",
trajectoryName = "Neuronal trajectory")
## `geom_smooth()` using formula 'y ~ x'
visualizeTFTrajectory(TF = TF,
obj = seu.sub.meso, pseudotimeColName = "pseudotime.meso",
trajectoryName = "Mesoderm trajectory")
## `geom_smooth()` using formula 'y ~ x'
visualizeTFTrajectory(TF = TF,
obj = seu.sub.und, pseudotimeColName = "pseudotime.und",
trajectoryName = "Unknown trajectory")
## `geom_smooth()` using formula 'y ~ x'
Try this with a few other TFs, which TFs give very different or very clear TF trajectories? Play around and identify candidate TFs that seem to be differential among the three classes we subset here.
We can also create a pseudotime TF heatmap based on TF activity scores for each sub trajectory. We again first create a custom function that we can call multiple times afterwards:
createHeatmap <- function(obj, pseudotimeColName, title = "Trajectory", topTFs = 100) {
# Code taken and adjusted from here: https://github.com/crickbabs/ZebrafishDevelopingHindbrainAtlas
TFdata = obj@assays$chromvar@data
# take X most variable TFs
TFdata.f = head(TFdata[order(rowSds(TFdata), decreasing = TRUE),], topTFs)
# TF normalization
pseudotimeData = obj@meta.data[pseudotimeColName]
# Fix to the same name here for easier code
colnames(pseudotimeData) = "pseudotime"
pseudotimeData = pseudotimeData %>% rownames_to_column("cellID") %>% arrange(pseudotime)
pt.matrix <- TFdata.f[, pseudotimeData$cellID]
pt.matrix <- t(apply(pt.matrix,1,function(x){smooth.spline(x,df=3)$y}))
pt.matrix <- t(apply(pt.matrix,1,function(x){(x-mean(x))/sd(x)}))
# rownames(pt.matrix) <- genes;
ht <- Heatmap(
pt.matrix,
column_title = paste0(title, ": top ", topTFs, " variable TFs"),
name = "z-score",
col = colorRamp2(seq(from=-2,to=2,length=11),rev(brewer.pal(11, "Spectral"))),
show_row_names = TRUE,
show_column_names = FALSE,
row_names_gp = gpar(fontsize = 5),
# km = 6, if cluster_rows = TRUE
row_title_rot = 0,
cluster_rows = TRUE, # TRUE
cluster_row_slices = FALSE,
cluster_columns = FALSE)
print(ht)
}
Similar to before, we can now plot the pseudotime TF heatmap for each of the three cell clusters:
createHeatmap(obj = seu.sub.neu, pseudotimeColName = "pseudotime.neu", title = "Neuronal trajectory", topTFs = 100)
createHeatmap(obj = seu.sub.meso, pseudotimeColName = "pseudotime.meso", title = "Mesoderm trajectory", topTFs = 100)
createHeatmap(obj = seu.sub.und, pseudotimeColName = "pseudotime.und", title = "Unknown trajectory", topTFs = 100)
This concludes the vignette, and from here, you can continue exploring the data, connecting different assays and data together, and make sense out of the data and explore new biology!
We reached the end of the vignette. We again save our updated
Seurat object to disk with a new name, in analogy to what
we did in the other vignettes.
saveRDS(seu.s, file = paste0(outFolder,"obj.filt.monocle.rds"))
It is good practice to print the so-called session info at the end of an R script, which prints all loaded libraries, their versions etc. This can be helpful for reproducibility and recapitulating which package versions have been used to produce the results obtained above.
sessionInfo()
## R version 4.2.0 (2022-04-22)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 20.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## attached base packages:
## [1] grid stats4 stats graphics grDevices utils datasets
## [8] methods base
##
## other attached packages:
## [1] RColorBrewer_1.1-3 circlize_0.4.14
## [3] ComplexHeatmap_2.12.0 scales_1.2.0
## [5] patchwork_1.1.1 forcats_0.5.1
## [7] stringr_1.4.0 dplyr_1.0.9
## [9] purrr_0.3.4 readr_2.1.2
## [11] tidyr_1.2.0 tibble_3.1.7
## [13] ggplot2_3.3.6 tidyverse_1.3.1
## [15] monocle3_1.0.0 SingleCellExperiment_1.18.0
## [17] SummarizedExperiment_1.26.1 GenomicRanges_1.48.0
## [19] GenomeInfoDb_1.32.1 IRanges_2.30.0
## [21] S4Vectors_0.34.0 MatrixGenerics_1.8.0
## [23] matrixStats_0.62.0 Biobase_2.56.0
## [25] BiocGenerics_0.42.0 SeuratWrappers_0.3.0
## [27] sp_1.4-7 SeuratObject_4.1.0
## [29] Seurat_4.1.1 Signac_1.6.0
##
## loaded via a namespace (and not attached):
## [1] utf8_1.2.2 reticulate_1.24 R.utils_2.11.0
## [4] tidyselect_1.1.2 htmlwidgets_1.5.4 docopt_0.7.1
## [7] BiocParallel_1.30.0 Rtsne_0.16 munsell_0.5.0
## [10] codetools_0.2-18 ica_1.0-2 future_1.25.0
## [13] miniUI_0.1.1.1 withr_2.5.0 spatstat.random_2.2-0
## [16] colorspace_2.0-3 progressr_0.10.0 highr_0.9
## [19] knitr_1.39 rstudioapi_0.13 ROCR_1.0-11
## [22] tensor_1.5 listenv_0.8.0 labeling_0.4.2
## [25] slam_0.1-50 GenomeInfoDbData_1.2.8 polyclip_1.10-0
## [28] farver_2.1.0 parallelly_1.31.1 vctrs_0.4.1
## [31] generics_0.1.2 xfun_0.30 lsa_0.73.2
## [34] ggseqlogo_0.1 doParallel_1.0.17 R6_2.5.1
## [37] clue_0.3-60 rsvd_1.0.5 bitops_1.0-7
## [40] spatstat.utils_2.3-0 DelayedArray_0.22.0 assertthat_0.2.1
## [43] promises_1.2.0.1 rgeos_0.5-9 gtable_0.3.0
## [46] Cairo_1.5-15 globals_0.14.0 goftest_1.2-3
## [49] rlang_1.0.2 RcppRoll_0.3.0 GlobalOptions_0.1.2
## [52] splines_4.2.0 lazyeval_0.2.2 spatstat.geom_2.4-0
## [55] broom_0.8.0 modelr_0.1.8 BiocManager_1.30.17
## [58] yaml_2.3.5 reshape2_1.4.4 abind_1.4-5
## [61] backports_1.4.1 httpuv_1.6.5 tools_4.2.0
## [64] ellipsis_0.3.2 spatstat.core_2.4-2 jquerylib_0.1.4
## [67] proxy_0.4-26 ggridges_0.5.3 Rcpp_1.0.8.3
## [70] plyr_1.8.7 zlibbioc_1.42.0 RCurl_1.98-1.6
## [73] rpart_4.1.16 deldir_1.0-6 GetoptLong_1.0.5
## [76] pbapply_1.5-0 viridis_0.6.2 cowplot_1.1.1
## [79] zoo_1.8-10 haven_2.5.0 ggrepel_0.9.1
## [82] cluster_2.1.3 fs_1.5.2 magrittr_2.0.3
## [85] data.table_1.14.2 scattermore_0.8 reprex_2.0.1
## [88] lmtest_0.9-40 RANN_2.6.1 SnowballC_0.7.0
## [91] fitdistrplus_1.1-8 hms_1.1.1 mime_0.12
## [94] evaluate_0.15 xtable_1.8-4 readxl_1.4.0
## [97] sparsesvd_0.2 shape_1.4.6 gridExtra_2.3
## [100] compiler_4.2.0 KernSmooth_2.23-20 crayon_1.5.1
## [103] R.oo_1.24.0 htmltools_0.5.2 tzdb_0.3.0
## [106] mgcv_1.8-40 later_1.3.0 lubridate_1.8.0
## [109] DBI_1.1.2 tweenr_1.0.2 dbplyr_2.1.1
## [112] MASS_7.3-56 leidenbase_0.1.9 Matrix_1.4-1
## [115] cli_3.3.0 R.methodsS3_1.8.1 parallel_4.2.0
## [118] igraph_1.3.1 pkgconfig_2.0.3 plotly_4.10.0
## [121] spatstat.sparse_2.1-1 foreach_1.5.2 xml2_1.3.3
## [124] bslib_0.3.1 XVector_0.36.0 rvest_1.0.2
## [127] digest_0.6.29 sctransform_0.3.3 RcppAnnoy_0.0.19
## [130] spatstat.data_2.2-0 Biostrings_2.64.0 cellranger_1.1.0
## [133] rmarkdown_2.14 leiden_0.3.10 fastmatch_1.1-3
## [136] uwot_0.1.11 shiny_1.7.1 Rsamtools_2.12.0
## [139] rjson_0.2.21 lifecycle_1.0.1 nlme_3.1-157
## [142] jsonlite_1.8.0 viridisLite_0.4.0 fansi_1.0.3
## [145] pillar_1.7.0 lattice_0.20-45 fastmap_1.1.0
## [148] httr_1.4.3 survival_3.2-13 glue_1.6.2
## [151] remotes_2.4.2 qlcMatrix_0.9.7 iterators_1.0.14
## [154] png_0.1-7 ggforce_0.3.3 stringi_1.7.6
## [157] sass_0.4.1 irlba_2.3.5 future.apply_1.9.0